You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

626 lines
23 KiB

"use client";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { PageLoadingSkeleton } from "@/components/Componentes/page-loading-skeleton";
import NavigationButton from "@/components/Componentes/navigation-button";
import { PageBackground } from "@/components/Componentes/page-background";
import {
hasQuestionAnswerValue,
QuestionAnswersProvider,
useQuestionAnswers,
} from "@/components/Componentes/question-answer-storage";
import QuestionExitNavigationButton from "@/components/Componentes/question-exit-navigation-button";
import QuestionRenderer from "@/components/Componentes/question-renderer";
import QuestionSectionFlow from "@/components/Componentes/question-section-flow";
import StickyHeader from "@/components/Componentes/sticky-header";
import TestIntroPage from "@/components/Componentes/test-intro-page";
import TestQuestionsFlow, {
type TestQuestion,
} from "@/components/Componentes/test-questions-flow";
import {
useGlasserQuestionsQuery,
useSubmitGlasserAssessmentMutation,
} from "@/hooks/marriage/use-glasser";
import {
useCattellQuestionsQuery,
useSubmitCattellAssessmentMutation,
} from "@/hooks/marriage/use-cattell";
import { useFormSchemaQuery } from "@/hooks/marriage/use-form-schema";
import { convertSchemaToFrontendItems, type QuestionField } from "@/lib/schema-adapter";
import { defaultLocale, type Locale } from "@/translations/config";
import { useI18n } from "@/translations/provider";
type QuestionDetailClientProps = {
closeLabel: string;
continueLabel: string;
description: string;
informationLabel: string;
itemSlug: string;
locale?: Locale;
questionsListHref: string;
title: string;
};
type StoredQuestionField = {
label?: string;
value?: unknown;
type?: string;
key?: string;
};
type StoredAnswers = {
fields?: StoredQuestionField[];
};
function getTestDraftStorageKey(slug: string) {
return `marriage:tests:${slug}:draft`;
}
function getQuestionStorageKey(slug: string) {
return `marriage:sections:${slug}:answers`;
}
function QuestionFlowWrapper({
visibleQuestions,
itemSlug,
dobQuestion,
continueLabel,
questionsListHref,
}: {
visibleQuestions: QuestionField[];
itemSlug: string;
dobQuestion?: QuestionField;
requiredQuestionsCount: number;
continueLabel: string;
questionsListHref: string;
}) {
const { getAnswerValue } = useQuestionAnswers();
// dynamicQuestions is now exactly what the backend gives as visible
const dynamicQuestions = visibleQuestions;
const requiredCount = useMemo(
() => dynamicQuestions.filter((q) => q.required).length,
[dynamicQuestions],
);
return (
<QuestionSectionFlow
key={itemSlug}
total={requiredCount}
continueLabel={continueLabel}
exitHref={questionsListHref}
optionalQuestionIndexes={dynamicQuestions.flatMap((question, index) =>
question.required ? [] : [index],
)}
questions={dynamicQuestions}
>
{dynamicQuestions.map((question, index) => {
const answer = getAnswerValue(question);
const hasAnswer = hasQuestionAnswerValue(answer ?? null);
let isAnswered = hasAnswer;
if (hasAnswer) {
const isEmailQuestion = question.type === "email" || question.validation?.format === "email";
if (isEmailQuestion) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
isAnswered = emailRegex.test(String(answer).trim());
} else if (question.type === "birthplace") {
const strVal = String(answer);
const parts = strVal.split(",").map((p) => p.trim());
isAnswered =
parts.length >= 2 && parts[0].length > 0 && parts[1].length > 0;
} else if (question.type === "checkbox") {
isAnswered = Array.isArray(answer) ? answer.length > 0 : hasAnswer;
}
}
return (
<div
key={question.id}
data-question-required={String(question.required)}
data-question-optional={String(!question.required)}
data-question-index={index}
data-question-original-index={question.order}
data-question-disabled="false"
data-question-answered={String(isAnswered)}
>
<QuestionRenderer
question={question}
dobQuestion={dobQuestion}
/>
</div>
);
})}
</QuestionSectionFlow>
);
}
export default function QuestionDetailClient({
closeLabel,
continueLabel,
description,
informationLabel,
itemSlug,
locale = defaultLocale,
questionsListHref,
title,
}: QuestionDetailClientProps) {
const router = useRouter();
const { dictionary: t } = useI18n();
const [isTestStarted, setIsTestStarted] = useState(false);
const [hasTestProgress, setHasTestProgress] = useState(false);
useEffect(() => {
if (typeof window !== "undefined") {
const draftKey = `marriage:tests:${itemSlug}:draft`;
const draftRaw = window.localStorage.getItem(draftKey);
if (draftRaw) {
try {
const parsed = JSON.parse(draftRaw);
if (
parsed &&
typeof parsed.answers === "object" &&
parsed.answers !== null &&
Object.keys(parsed.answers).length > 0
) {
setHasTestProgress(true);
return;
}
} catch {}
}
setHasTestProgress(false);
}
}, [itemSlug, isTestStarted]);
const { data: schema, isLoading: isSchemaLoading } = useFormSchemaQuery("profile", locale);
const items = useMemo(() => convertSchemaToFrontendItems(schema, locale), [schema, locale]);
const item = items.find((i) => i.slug === itemSlug);
const isCattellSlug = itemSlug === "personality_test";
const isGlasserSlug = itemSlug === "glasser_5_needs_test";
const cattellQuery = useCattellQuestionsQuery(locale, {
enabled: isCattellSlug && isTestStarted,
retry: 0,
});
const submitCattellMutation = useSubmitCattellAssessmentMutation();
const glasserQuery = useGlasserQuestionsQuery(locale, {
enabled: isGlasserSlug && isTestStarted,
retry: 0,
});
const submitGlasserMutation = useSubmitGlasserAssessmentMutation();
const cattellTestQuestions: TestQuestion[] = useMemo(() => {
const questionsList = cattellQuery.data?.questions || [];
// Strict schema validation for Cattell
const isValidCattell = (q: any) =>
q.question_number &&
q.text &&
q.options &&
q.options.length === 3 &&
q.options.every((o: any) => o.id && o.label && o.value);
if (questionsList.length > 0 && !questionsList.every(isValidCattell)) {
console.error("Invalid Cattell API response schema");
return [];
}
return questionsList.map((q) => ({
id: q.question_number,
text: q.text,
options: q.options || [],
}));
}, [cattellQuery.data]);
const glasserTestQuestions: TestQuestion[] = useMemo(() => {
const questionsList = glasserQuery.data?.questions || [];
// Strict schema validation for Glasser
const isValidGlasser = (q: any) =>
q.question_number &&
q.text &&
q.options &&
q.options.length === 5 &&
q.options.every((o: any) => o.id && o.label && typeof o.value === 'number' && o.value >= 1 && o.value <= 5);
if (questionsList.length > 0 && !questionsList.every(isValidGlasser)) {
console.error("Invalid Glasser API response schema");
return [];
}
return questionsList.map((q) => ({
id: q.question_number,
text: q.text,
info:
"factor" in q
? (q.factor as string)
: "factor_code" in q
? (q.factor_code as string)
: undefined,
options: q.options || [],
}));
}, [glasserQuery.data]);
const visibleQuestions = useMemo(() => {
if (!item) {
return [];
}
return item.questions
.filter((question) => (question as any).isVisible !== false)
.map((question) => ({
...question,
required: Boolean(question.required),
}));
}, [item]);
const requiredQuestionsCount = useMemo(
() => visibleQuestions.filter((q) => q.required).length,
[visibleQuestions],
);
useEffect(() => {
if (!isSchemaLoading && !item) {
router.replace(questionsListHref);
}
}, [isSchemaLoading, item, questionsListHref, router]);
if (isSchemaLoading) {
return (
<PageLoadingSkeleton
compact
variant={isCattellSlug || isGlasserSlug ? "test" : "questions"}
/>
);
} else if (!item) {
return null;
}
if (item && item.questions.length === 0) {
if (isTestStarted) {
const isQuestionsLoading = isCattellSlug
? cattellQuery.isLoading
: isGlasserSlug
? glasserQuery.isLoading
: false;
if (isQuestionsLoading) {
return <PageLoadingSkeleton compact variant="test" />;
}
const activeTestQuestions = isCattellSlug
? cattellTestQuestions
: isGlasserSlug
? glasserTestQuestions
: [];
if (activeTestQuestions.length === 0) {
const isError = isCattellSlug
? cattellQuery.isError
: isGlasserSlug
? glasserQuery.isError
: false;
const refetch = isCattellSlug
? cattellQuery.refetch
: glasserQuery.refetch;
return (
<>
<PageBackground disabled />
<main className="-mx-[17px] flex h-svh flex-col items-center justify-center gap-4 bg-[#F7F1F0] px-6 text-center">
<p className="font-semibold text-[#1B1B1B]">
{isError
? locale === "fa"
? "ط®ط·ط§ ط¯ط± ط¯ط±غŒط§ظپطھ ط³ظˆط§ظ„ط§طھ ط§ط² ط³ط±ظˆط±. ظ„ط·ظپط§ظ‹ ط§ط² ط§طھطµط§ظ„ ط§غŒظ†طھط±ظ†طھ غŒط§ ظˆط±ظˆط¯ ط¨ظ‡ ط­ط³ط§ط¨ ع©ط§ط±ط¨ط±غŒ ط§ط·ظ…غŒظ†ط§ظ† ط­ط§طµظ„ ع©ظ†غŒط¯."
: "Failed to load questions from server. Please check your connection or login status."
: locale === "fa"
? "ط³ظˆط§ظ„ط§طھغŒ ط¨ط±ط§غŒ ط§غŒظ† ط¢ط²ظ…ظˆظ† غŒط§ظپطھ ظ†ط´ط¯."
: "No questions found for this test."}
</p>
<div className="flex gap-3">
<button
type="button"
onClick={() => setIsTestStarted(false)}
className="rounded-xl bg-[#EFEFEF] px-4 py-2 text-sm font-semibold text-[#1B1B1B]"
>
{closeLabel}
</button>
<button
type="button"
onClick={() => refetch()}
className="rounded-xl bg-[#F2465F] px-4 py-2 text-sm font-semibold text-white shadow-md"
>
{locale === "fa" ? "طھظ„ط§ط´ ظ…ط¬ط¯ط¯" : "Retry"}
</button>
</div>
</main>
</>
);
}
const handleTestFinish = async (
answers: Record<number, string | number>,
) => {
if (isCattellSlug) {
const responses = Object.entries(answers).map(([qNum, option]) => ({
question_number: Number(qNum),
option: String(option),
}));
await submitCattellMutation.mutateAsync({ responses });
try {
window.localStorage.setItem(
getQuestionStorageKey(item.slug),
JSON.stringify({ completed: true }),
);
} catch {}
} else if (isGlasserSlug) {
const responses = Object.entries(answers).map(([qNum, score]) => ({
question_number: Number(qNum),
score: Number(score),
}));
await submitGlasserMutation.mutateAsync({ responses });
try {
window.localStorage.setItem(
getQuestionStorageKey(item.slug),
JSON.stringify({ completed: true }),
);
} catch {}
}
};
return (
<TestQuestionsFlow
title={item.title}
questions={activeTestQuestions}
closeLabel={closeLabel}
informationLabel={informationLabel}
onClose={() => setIsTestStarted(false)}
onFinish={handleTestFinish}
draftStorageKey={getTestDraftStorageKey(item.slug)}
/>
);
}
const bulletKey =
item.slug === "glasser_5_needs_test" ? "glasser" : "personality";
const bullets =
bulletKey === "glasser"
? [
t[
"Understanding your five basic needs helps you recognize what truly drives your behavior in relationships."
],
t[
"Taking this test is not mandatory, but it will help you better understand your priorities and find a more compatible spouse."
],
t[
"By identifying your strongest needs, you can better communicate your expectations and build healthier relationships."
],
]
: [
t[
"Do you struggle to understand the underlying psychological drivers that dictate your interpersonal connections and communication barriers?"
],
t[
"Taking this test is not mandatory, but it will help you better search for a spouse. The Personality test is a test for self-knowledge and better understanding of your spouse."
],
t[
'Do you operate based on superficial behavioral adaptations, or are you aware of the deep "source traits" that fundamentally control your decision-making processes?'
],
];
return (
<>
<PageBackground disabled />
<main className="-mx-[17px] flex h-svh flex-col overflow-hidden bg-[#F7F1F0]">
<StickyHeader sticky={false} className="shrink-0">
<div className="flex items-center gap-4">
<NavigationButton
className="shrink-0"
variant="transparent"
icon="close"
iconLabel={closeLabel}
/>
<h1 className="min-w-0 flex-1 text-center text-[14px] font-semibold text-white truncate">
{item.title}
</h1>
<NavigationButton
className="shrink-0"
variant="transparent"
icon="info"
iconLabel={informationLabel}
helpTitle={item.title}
helpDescription={description}
/>
</div>
</StickyHeader>
<div className="mx-auto flex w-full max-w-md flex-1 flex-col px-[17px] pt-3 min-h-0">
<TestIntroPage
title={item.title}
estimateTime={item.estimate}
description={t["Estimate time"]}
bulletPoints={
isCattellSlug || isGlasserSlug ? undefined : bullets
}
disclaimerText={
t[
"All provided information is held in strict confidence and will not be disclosed to third parties. This data is utilized exclusively to ensure optimal matchmaking accuracy. Consequently, it is imperative that this assessment be completed with the utmost diligence, precision, and integrity."
]
}
startLabel={hasTestProgress ? t["Continue"] : t["Start"]}
onStart={() => {
setIsTestStarted(true);
}}
>
{isCattellSlug ? (
<div className="mt-5 text-left">
<ul className="space-y-4">
{[
{
title: t["Understanding Personality Traits"],
desc: t[
"Helps provide an overall picture of traits such as sociability, independence, emotional sensitivity, and interpersonal style."
],
},
{
title: t["Assessing Communication Style"],
desc: t[
"Shows how a person typically communicates, expresses emotions, and builds closeness in relationships."
],
},
{
title:
t["Understanding Responses to Stress and Conflict"],
desc: t[
"Offers insight into emotional stability, tension levels, and how a person may react in difficult or stressful situations."
],
},
{
title: t["Assessing Independence and Decision-Making"],
desc: t[
"Helps identify the person’s level of independence, assertiveness, and preference for individual or shared decision-making."
],
},
{
title:
t["Identifying Potential Differences and Challenges"],
desc: t[
"Comparing two individuals’ results can highlight personality differences that may require attention in a long-term relationship."
],
},
{
title: t["Supporting Better Match Recommendations"],
desc: t[
"Alongside interviews and other relationship criteria, personality assessment can help make the matching process more targeted and improve the evaluation of compatibility."
],
},
].map((bullet, index) => (
<li key={index} className="min-w-0">
<h4 className="group-12 font-bold text-[#1B1B1B] leading-snug">
{bullet.title}
</h4>
<p className="group-12 text-[#5A5A5A] mt-1 leading-[1.55] text-justify">
{bullet.desc}
</p>
</li>
))}
</ul>
</div>
) : isGlasserSlug ? (
<div className="mt-5 text-left">
<ul className="space-y-4">
{[
{
title: t["Understanding Core Psychological Needs"],
desc: t[
"Helps identify the importance of the five basic needs—love and belonging, power, freedom, fun, and survival—in each person’s life."
],
},
{
title: t["Recognizing Relationship Expectations"],
desc: t[
"Provides insight into what each person expects from a relationship, such as closeness, independence, security, achievement, or shared enjoyment."
],
},
{
title: t["Assessing Personality Compatibility"],
desc: t[
"Helps compare personality traits, behavioral tendencies, and interaction styles to identify areas of compatibility between two individuals."
],
},
{
title: t["Identifying Potential Sources of Conflict"],
desc: t[
"Differences in needs or personality styles can highlight areas where misunderstandings, tension, or disagreements may arise in the relationship."
],
},
{
title: t["Improving Mutual Understanding"],
desc: t[
"Helps individuals better understand their own needs as well as their partner’s motivations, preferences, and emotional priorities."
],
},
{
title:
t["Supporting More Suitable Match Recommendations"],
desc: t[
"Combining needs and personality assessments with interviews and other marriage criteria can help make partner recommendations more personalized and well-matched."
],
},
].map((bullet, index) => (
<li key={index} className="min-w-0">
<h4 className="group-12 font-bold text-[#1B1B1B] leading-snug">
{bullet.title}
</h4>
<p className="group-12 text-[#5A5A5A] mt-1 leading-[1.55] text-justify">
{bullet.desc}
</p>
</li>
))}
</ul>
</div>
) : null}
</TestIntroPage>
</div>
</main>
</>
);
}
const dobQuestion = visibleQuestions.find(
(question) => question.ui_config?.isDob === true || question.type === "date",
);
return (
<>
<PageBackground disabled />
<QuestionAnswersProvider
slug={item.slug}
questions={visibleQuestions}
locale={locale}
schemaVersion={schema?.version ?? 1}
>
<main className="-mx-[17px] flex h-svh flex-col overflow-hidden bg-[#F7F1F0]">
<StickyHeader sticky={false} className="shrink-0">
<div className="flex items-center gap-4">
<QuestionExitNavigationButton
className="shrink-0"
variant="transparent"
icon="close"
iconLabel={closeLabel}
exitHref={questionsListHref}
/>
<h1 className="min-w-0 flex-1 text-center text-[14px] font-semibold text-white truncate">
{item.title}
</h1>
<NavigationButton
className="shrink-0"
variant="transparent"
icon="info"
iconLabel={informationLabel}
helpTitle={item.title}
helpDescription={description}
/>
</div>
</StickyHeader>
<div className="mx-auto flex w-full max-w-md flex-1 flex-col px-[17px] pt-3 min-h-0">
<QuestionFlowWrapper
visibleQuestions={visibleQuestions}
itemSlug={item.slug}
dobQuestion={dobQuestion}
requiredQuestionsCount={requiredQuestionsCount}
continueLabel={continueLabel}
questionsListHref={questionsListHref}
/>
</div>
</main>
</QuestionAnswersProvider>
</>
);
}